Automatización de la Selección de Visualizaciones de Datos para la Alfabetización de Datos - Visualización Interactiva Mockup

2024-2 · Máster universitario en Ciencia de datos (Data science)

Estudios de Informática, Multimedia y Telecomunicación

 
  1. Carga de un conjunto de datos
  2. Análisis de los datos
    2.1 Análisis estadístico básico
    2.2 Análisis exploratorio de los datos
  3. Preprocesado de los datos
Nombre y apellidos: Inmaculada Pizarro Moreno
In [1]:
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn import datasets
from sklearn import preprocessing
from sklearn.manifold import TSNE
from sklearn.decomposition import PCA
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN
from imblearn.under_sampling import RandomUnderSampler, TomekLinks, EditedNearestNeighbours

import matplotlib
import matplotlib.pyplot as plt

pd.set_option('display.max_columns', None)
seed = 100

%matplotlib inline

1. Carga del conjunto de datos¶

Se eligieron varios datasets de Eurostat sobre migración y población. En particular se han utilizado las bases de datos:

  • Immigration by age group, sex and country of birth (migr_imm3ctb). Proporciona datos sobre los flujos internacionales de inmigración según varias clasificaciones, como edad, sexo, país de nacimiento.
  • Emigration by age group, sex and country of birth (migr_emi4ctb). Proporciona datos sobre los flujos internacionales de emigración según varias clasificaciones, como edad, sexo, país de nacimiento, país de ciudadanía.
  • Population change - Demographic balance and crude rates at national level (demo_gind)
  • Las dos primeras bases de datos se obtuvieron del siguiente URL: https://ec.europa.eu/eurostat/web/migration-asylum/international-migration-citizenship/database La base de datos de población se obtuve de esta otra URL: https://ec.europa.eu/eurostat/databrowser/view/demo_gind/default/table?lang=en

    In [2]:
    import os
    import pandas as pd
    
    # Especificar la carpeta donde están los archivos
    carpeta = r"C:\Users\34617\Documents\MASTER_DATA_SCIENCE\TFM\visualizacion interactiva\FINAL"
    
    # Leer los archivos Excel
    countrycodes = pd.read_excel(os.path.join(carpeta, "Country_codes_regions2.xlsx"))
    countrybirthcodes = pd.read_excel(os.path.join(carpeta, "Country_codes_regions_birth2.xlsx"))
    inmigration = pd.read_excel(os.path.join(carpeta, "Inmigrations.xlsx"))
    emigration = pd.read_excel(os.path.join(carpeta, "Emigrations.xlsx"))
    population = pd.read_excel(os.path.join(carpeta, "Population_indicators.xlsx"))
    
    In [3]:
    # Mostrar los DataFrames para verificar que se cargaron correctamente
    print(countrycodes.describe(include='all'))
    print(countrybirthcodes.describe(include='all'))
    print(inmigration.describe(include='all'))
    print(emigration.describe(include='all'))
    print(population.describe(include='all'))
    #print(countrycodes.head())
    #print(countrybirthcodes.head())
    #print(inmigration.head())
    #print(emigration.head())
    #print(population.head())
    
           reporting_country reporting_country_name reporting_country_region  \
    count                285                    286                      286   
    unique               284                    281                       12   
    top                   TW                  Other                   Africa   
    freq                   2                      2                       66   
    
           reporting_country_sub-region  
    count                           286  
    unique                           41  
    top              Sub-Saharan Africa  
    freq                             53  
           country_birth country_birth_name country_birth_region  \
    count            285                286                  286   
    unique           284                281                   12   
    top               TW              Other               Africa   
    freq               2                  2                   66   
    
           country_birth_sub-region  
    count                       286  
    unique                       41  
    top          Sub-Saharan Africa  
    freq                         53  
              freq     age   agedef country_birth    unit     sex  \
    count   739696  739696   739696        736856  739696  739696   
    unique       1      27        2           251       1       3   
    top          A   TOTAL  COMPLET         TOTAL      NR       T   
    freq    739696   77872   378432         13912  739696  251304   
    mean       NaN     NaN      NaN           NaN     NaN     NaN   
    std        NaN     NaN      NaN           NaN     NaN     NaN   
    min        NaN     NaN      NaN           NaN     NaN     NaN   
    25%        NaN     NaN      NaN           NaN     NaN     NaN   
    50%        NaN     NaN      NaN           NaN     NaN     NaN   
    75%        NaN     NaN      NaN           NaN     NaN     NaN   
    max        NaN     NaN      NaN           NaN     NaN     NaN   
    
           reporting_country  conteo_nulos           Year  Inmigrations  
    count             739696      739696.0  739696.000000  7.396960e+05  
    unique                27           NaN            NaN           NaN  
    top                   HR           NaN            NaN           NaN  
    freq              136272           NaN            NaN           NaN  
    mean                 NaN           0.0    2018.500000  7.308314e+02  
    std                  NaN           0.0       2.291289  1.324543e+04  
    min                  NaN           0.0    2015.000000  0.000000e+00  
    25%                  NaN           0.0    2016.750000  0.000000e+00  
    50%                  NaN           0.0    2018.500000  0.000000e+00  
    75%                  NaN           0.0    2020.250000  9.000000e+00  
    max                  NaN           0.0    2022.000000  1.943445e+06  
              freq     age   agedef country_birth    unit     sex  \
    count   442168  442168   442168        440400  442168  442168   
    unique       1      26        2           249       1       3   
    top          A   TOTAL  COMPLET         TOTAL      NR       T   
    freq    442168   68176   269672          5352  442168  150200   
    mean       NaN     NaN      NaN           NaN     NaN     NaN   
    std        NaN     NaN      NaN           NaN     NaN     NaN   
    min        NaN     NaN      NaN           NaN     NaN     NaN   
    25%        NaN     NaN      NaN           NaN     NaN     NaN   
    50%        NaN     NaN      NaN           NaN     NaN     NaN   
    75%        NaN     NaN      NaN           NaN     NaN     NaN   
    max        NaN     NaN      NaN           NaN     NaN     NaN   
    
           reporting_country  conteo_nulos          Year    Emigrations  
    count             442168      442168.0  442168.00000  442168.000000  
    unique                13           NaN           NaN            NaN  
    top                   IT           NaN           NaN            NaN  
    freq              125600           NaN           NaN            NaN  
    mean                 NaN           0.0    2018.50000     298.117697  
    std                  NaN           0.0       2.29129    5058.903108  
    min                  NaN           0.0    2015.00000       0.000000  
    25%                  NaN           0.0    2016.75000       0.000000  
    50%                  NaN           0.0    2018.50000       0.000000  
    75%                  NaN           0.0    2020.25000       4.000000  
    max                  NaN           0.0    2022.00000  696866.000000  
           freq reporting_country         Year  AVG_TOTAL_POPULATION  \
    count   460               460   460.000000          4.100000e+02   
    unique    1                46          NaN                   NaN   
    top       A                AL          NaN                   NaN   
    freq    460                10          NaN                   NaN   
    mean    NaN               NaN  2019.500000          3.941081e+07   
    std     NaN               NaN     2.875408          9.371651e+07   
    min     NaN               NaN  2015.000000          0.000000e+00   
    25%     NaN               NaN  2017.000000          1.930942e+06   
    50%     NaN               NaN  2019.500000          6.896632e+06   
    75%     NaN               NaN  2022.000000          1.741734e+07   
    max     NaN               NaN  2024.000000          4.483829e+08   
    
                   DEATH  FEMENINE_JAN           JAN  
    count   4.100000e+02  4.300000e+02  4.600000e+02  
    unique           NaN           NaN           NaN  
    top              NaN           NaN           NaN  
    freq             NaN           NaN           NaN  
    mean    4.026899e+05  1.919619e+07  3.922861e+07  
    std     1.001969e+06  4.692074e+07  9.325904e+07  
    min     0.000000e+00  0.000000e+00  0.000000e+00  
    25%     1.763300e+04  9.462492e+05  2.551962e+06  
    50%     6.048000e+04  3.356255e+06  6.823312e+06  
    75%     1.409748e+05  8.237585e+06  1.731352e+07  
    max     5.297333e+06  2.294311e+08  4.492066e+08  
    
    In [4]:
    #verifico si hay valores nulos en los datasets importados de eurostar
    
    nulos = inmigration.isna().any().any()  
    
    if nulos:
        print("Algunos valores en el dataset inmigración son nulos")
        # Mostrar las columnas que contienen valores nulos
        columnas_con_nulos = inmigration.columns[inmigration.isna().any()]
        print("Columnas con valores nulos en inmig:", columnas_con_nulos)
    else:
        print("Ningun valor en el dataset inmigración son nulos")
        
    
    nulos = emigration.isna().any().any()  
    
    if nulos:
        print("Algunos valores en el dataset emigracion son nulos")
        # Mostrar las columnas que contienen valores nulos
        columnas_con_nulos = emigration.columns[emigration.isna().any()]
        print("Columnas con valores nulos en emig:", columnas_con_nulos)
    
    else:
        print("Ningun valor en el dataset emigracion son nulos")
    
    nulos = population.isna().any().any()  
    
    if nulos:
        print("Algunos valores en el dataset population son nulos")
        # Mostrar las columnas que contienen valores nulos
        columnas_con_nulos = population.columns[population.isna().any()]
        print("Columnas con valores nulos en pp:", columnas_con_nulos)
    
    else:
        print("Ningun valor en el dataset population son nulos")
       
    
    Algunos valores en el dataset inmigración son nulos
    Columnas con valores nulos en inmig: Index(['country_birth'], dtype='object')
    Algunos valores en el dataset emigracion son nulos
    Columnas con valores nulos en emig: Index(['country_birth'], dtype='object')
    Algunos valores en el dataset population son nulos
    Columnas con valores nulos en pp: Index(['AVG_TOTAL_POPULATION', 'DEATH', 'FEMENINE_JAN'], dtype='object')
    
    In [5]:
    # si hay nulos remplazo por UNKNOWN pero en population por 0
    
    # Reemplazo valores nulos en country birth por UNKNOWN y vuelvo a comprobar
    conteo_nulos = inmigration['country_birth'].isna().sum()
    print(f"Cantidad de valores nulos en inmigration 'country_birth': {conteo_nulos}")
    #print(conteos_columna)
    columnas_a_reemplazar = ['country_birth']  # Lista de columnas que deseas modificar
    inmigration[columnas_a_reemplazar] = inmigration[columnas_a_reemplazar].fillna('UNK')
    conteo_nulos = inmigration['country_birth'].isna().sum()
    print(f"Cantidad de valores nulos en 'country_birth': {conteo_nulos}")
     
    
    # Reemplazo valores nulos en country birth por UNKNOWN y vuelvo a comprobar
    #conteos_columna = emigration['country_birth'].value_counts(dropna=False)
    conteo_nulos = emigration['country_birth'].isna().sum()
    print(f"Cantidad de valores nulos en 'country_birth': {conteo_nulos}")
    #print(conteos_columna)
    columnas_a_reemplazar = ['country_birth']  # Lista de columnas que deseas modificar
    emigration[columnas_a_reemplazar] = emigration[columnas_a_reemplazar].fillna('UNK')
    conteo_nulos = emigration['country_birth'].isna().sum()
    print(f"Cantidad de valores nulos en 'country_birth' tras remplazo: {conteo_nulos}")
    #conteos_columna = emigration['country_birth'].value_counts(dropna=False)
    #print(conteos_columna)  
    
    # Contar valores nulos en las columnas 'AVG_TOTAL_POPULATION', 'DEATH', 'FEMENINE_JAN'
    conteo_nulos = population[['AVG_TOTAL_POPULATION', 'DEATH', 'FEMENINE_JAN']].isna().sum()
    print(f"Cantidad de valores nulos antes del reemplazo:\n{conteo_nulos}")
    
    # Reemplazar valores nulos en las columnas especificadas por 0
    columnas_a_reemplazar = ['AVG_TOTAL_POPULATION', 'DEATH', 'FEMENINE_JAN']
    population[columnas_a_reemplazar] = population[columnas_a_reemplazar].fillna(0)
    
    # Contar valores nulos en las columnas 'AVG_TOTAL_POPULATION', 'DEATH', 'FEMENINE_JAN'
    conteo_nulos = population[['AVG_TOTAL_POPULATION', 'DEATH', 'FEMENINE_JAN']].isna().sum()
    print(f"Cantidad de valores nulos después del reemplazo:\n{conteo_nulos}")
    
    Cantidad de valores nulos en inmigration 'country_birth': 2840
    Cantidad de valores nulos en 'country_birth': 0
    Cantidad de valores nulos en 'country_birth': 1768
    Cantidad de valores nulos en 'country_birth': 0
    Cantidad de valores nulos antes del reemplazo:
    AVG_TOTAL_POPULATION    50
    DEATH                   50
    FEMENINE_JAN            30
    dtype: int64
    
    In [6]:
    # Convertir la columna "Year" a entero en ambos DataFrames
    inmigration["Year"] = inmigration["Year"].astype(int)
    emigration["Year"] = emigration["Year"].astype(int)
    
    inmigration = inmigration.drop(columns=["freq", "agedef", "unit","conteo_nulos"], errors="ignore")
    emigration = emigration.drop(columns=["freq", "agedef", "unit","conteo_nulos"], errors="ignore")
    
    
    # Paso 1: Uno inmigration con population y seleccionar columnas - en este caso nos interesa linkar por pais de nacimiento al estudiar las inmigraciones 
    # en relacion a la seguridad y poblacion del pais del que vienen (se ha tomado el pais de nacimiento como el de origen)
    merged_data_inmi = (
        inmigration
        .merge(population, on=["Year", "reporting_country"], how="inner")
    )
    
    # Elimino la columna 'reporting_country_y'  (que proviene de population_i)
    #merged_data_inmi = merged_data_inmi.drop(columns=["reporting_country_y"])
    
    # Asegurarse de que 'reporting_country_x' tenga el nombre correcto
    #merged_data_inmi = merged_data_inmi.rename(columns={"reporting_country_x": "reporting_country"})
    
    # Paso 2: uno con los países, regiones etc..
    merged_data_inmi = (
        merged_data_inmi
        .merge(countrycodes, on="reporting_country", how="left")
        .merge(countrybirthcodes, on="country_birth", how="left")
    )
    
    # Paso 3: Repetp el proceso para "emigration" pero linkar por el pais que reporta para ver la poblacion y muertes del pais de emigracion
    merged_data_emig = (
        emigration
        .merge(population, on=["Year", "reporting_country"], how="inner")
    )
    
    # Elimino la columna 'country_birth_y'  (que proviene de population_e)
    #merged_data_emig = merged_data_emig.drop(columns=["country_birth_y"])
    
    # Asegurarse de que 'country_birth_x' tenga el nombre correcto
    #merged_data_emig = merged_data_emig.rename(columns={"country_birth_x": "country_birth"})
    
    merged_data_emig = (
        merged_data_emig
        .merge(countrycodes, on="reporting_country", how="left")
        .merge(countrybirthcodes, on="country_birth", how="left")
    )
    
    In [14]:
    print(merged_data_inmi.columns)
    print(merged_data_emig.columns)
    # Poner todas las columnas en minusculas
    merged_data_inmi.columns = merged_data_inmi.columns.str.lower()
    # Poner todas las columnas en minusculas
    merged_data_emig.columns = merged_data_emig.columns.str.lower()
    #print(merged_data_inmi.columns)
    #print(merged_data_emig.columns)
    # orden
    column_order_i = ['year','age', 'sex', 
                      'reporting_country', 'reporting_country_name', 'reporting_country_region','reporting_country_sub-region', 
                      'country_birth', 'country_birth_name', 'country_birth_region', 'country_birth_sub-region',
                       'inmigrations', 'avg_total_population', 'death', 'femenine_jan','jan' ]
    column_order_e = ['year','age', 'sex',
                       'reporting_country', 'reporting_country_name', 'reporting_country_region','reporting_country_sub-region', 
                      'country_birth','country_birth_name','country_birth_region', 'country_birth_sub-region',
                       'emigrations', 'avg_total_population', 'death', 'femenine_jan','jan']
    # Reordenar las columnas
    merged_data_inmi = merged_data_inmi[column_order_i]
    # Reordenar las columnas
    merged_data_emig = merged_data_emig[column_order_e]
    print(merged_data_inmi.describe(include='all'))
    print(merged_data_emig.describe(include='all'))
    
    Index(['year', 'age', 'sex', 'reporting_country', 'reporting_country_name',
           'reporting_country_region', 'reporting_country_sub-region',
           'country_birth', 'country_birth_name', 'country_birth_region',
           'country_birth_sub-region', 'inmigrations', 'avg_total_population',
           'death', 'femenine_jan', 'jan'],
          dtype='object')
    Index(['year', 'age', 'sex', 'reporting_country', 'reporting_country_name',
           'reporting_country_region', 'reporting_country_sub-region',
           'country_birth', 'country_birth_name', 'country_birth_region',
           'country_birth_sub-region', 'emigrations', 'avg_total_population',
           'death', 'femenine_jan', 'jan'],
          dtype='object')
                year     age     sex reporting_country reporting_country_name  \
    count   742776.0  742776  742776            742776                 742776   
    unique       8.0      27       3                27                     27   
    top       2015.0   TOTAL       T                HR                Croatia   
    freq     92847.0   78160  252344            136824                 136824   
    mean         NaN     NaN     NaN               NaN                    NaN   
    std          NaN     NaN     NaN               NaN                    NaN   
    min          NaN     NaN     NaN               NaN                    NaN   
    25%          NaN     NaN     NaN               NaN                    NaN   
    50%          NaN     NaN     NaN               NaN                    NaN   
    75%          NaN     NaN     NaN               NaN                    NaN   
    max          NaN     NaN     NaN               NaN                    NaN   
    
           reporting_country_region reporting_country_sub-region country_birth  \
    count                    742776                       742776        742776   
    unique                        1                            4           251   
    top                      Europe              Southern Europe         TOTAL   
    freq                     742776                       392280         13912   
    mean                        NaN                          NaN           NaN   
    std                         NaN                          NaN           NaN   
    min                         NaN                          NaN           NaN   
    25%                         NaN                          NaN           NaN   
    50%                         NaN                          NaN           NaN   
    75%                         NaN                          NaN           NaN   
    max                         NaN                          NaN           NaN   
    
           country_birth_name country_birth_region country_birth_sub-region  \
    count              742776               742776                   742776   
    unique                250                   12                       41   
    top                 Other               Europe       Sub-Saharan Africa   
    freq                20112               179840                   136152   
    mean                  NaN                  NaN                      NaN   
    std                   NaN                  NaN                      NaN   
    min                   NaN                  NaN                      NaN   
    25%                   NaN                  NaN                      NaN   
    50%                   NaN                  NaN                      NaN   
    75%                   NaN                  NaN                      NaN   
    max                   NaN                  NaN                      NaN   
    
            inmigrations  avg_total_population         death  femenine_jan  \
    count   7.427760e+05          7.427760e+05  7.427760e+05  7.427760e+05   
    unique           NaN                   NaN           NaN           NaN   
    top              NaN                   NaN           NaN           NaN   
    freq             NaN                   NaN           NaN           NaN   
    mean    7.278536e+02          1.649419e+07  1.772644e+05  8.431840e+06   
    std     1.321802e+04          2.067951e+07  2.254252e+05  1.060170e+07   
    min     0.000000e+00          3.749400e+04  2.490000e+02  1.881300e+04   
    25%     0.000000e+00          2.842639e+06  4.084000e+04  1.544399e+06   
    50%     0.000000e+00          5.276968e+06  5.702300e+04  2.609187e+06   
    75%     9.000000e+00          1.734487e+07  1.533630e+05  8.701077e+06   
    max     1.943445e+06          8.379798e+07  1.066341e+06  4.217034e+07   
    
                     jan  
    count   7.427760e+05  
    unique           NaN  
    top              NaN  
    freq             NaN  
    mean    1.648687e+07  
    std     2.068275e+07  
    min     3.736600e+04  
    25%     2.859077e+06  
    50%     5.258317e+06  
    75%     1.728216e+07  
    max     8.323712e+07  
                year     age     sex reporting_country reporting_country_name  \
    count   443912.0  443912  443912            443912                 443912   
    unique       8.0      26       3                13                     13   
    top       2015.0   TOTAL       T                IT                  Italy   
    freq     55489.0   68432  150776            126152                 126152   
    mean         NaN     NaN     NaN               NaN                    NaN   
    std          NaN     NaN     NaN               NaN                    NaN   
    min          NaN     NaN     NaN               NaN                    NaN   
    25%          NaN     NaN     NaN               NaN                    NaN   
    50%          NaN     NaN     NaN               NaN                    NaN   
    75%          NaN     NaN     NaN               NaN                    NaN   
    max          NaN     NaN     NaN               NaN                    NaN   
    
           reporting_country_region reporting_country_sub-region country_birth  \
    count                    443912                       443912        443912   
    unique                        1                            4           249   
    top                      Europe              Southern Europe         TOTAL   
    freq                     443912                       267952          5352   
    mean                        NaN                          NaN           NaN   
    std                         NaN                          NaN           NaN   
    min                         NaN                          NaN           NaN   
    25%                         NaN                          NaN           NaN   
    50%                         NaN                          NaN           NaN   
    75%                         NaN                          NaN           NaN   
    max                         NaN                          NaN           NaN   
    
           country_birth_name country_birth_region country_birth_sub-region  \
    count              443912               443912                   443912   
    unique                248                   12                       41   
    top                 Other               Europe       Sub-Saharan Africa   
    freq                 6136               105840                    87528   
    mean                  NaN                  NaN                      NaN   
    std                   NaN                  NaN                      NaN   
    min                   NaN                  NaN                      NaN   
    25%                   NaN                  NaN                      NaN   
    50%                   NaN                  NaN                      NaN   
    75%                   NaN                  NaN                      NaN   
    max                   NaN                  NaN                      NaN   
    
              emigrations  avg_total_population          death  femenine_jan  \
    count   443912.000000          4.439120e+05  443912.000000  4.439120e+05   
    unique            NaN                   NaN            NaN           NaN   
    top               NaN                   NaN            NaN           NaN   
    freq              NaN                   NaN            NaN           NaN   
    mean       296.952286          2.163009e+07  237046.914922  1.110266e+07   
    std       5048.989940          2.612338e+07  287879.408046  1.340806e+07   
    min          0.000000          3.749400e+04     249.000000  1.881300e+04   
    25%          0.000000          2.108079e+06   23261.000000  1.049039e+06   
    50%          0.000000          2.877325e+06   41776.000000  1.563839e+06   
    75%          4.000000          5.901367e+07  615261.000000  3.021118e+07   
    max     696866.000000          6.022960e+07  740317.000000  3.106718e+07   
    
                     jan  
    count   4.439120e+05  
    unique           NaN  
    top              NaN  
    freq             NaN  
    mean    2.163589e+07  
    std     2.613781e+07  
    min     3.736600e+04  
    25%     2.107180e+06  
    50%     2.895573e+06  
    75%     5.903013e+07  
    max     6.029550e+07  
    
    In [15]:
    numerical_i = ['inmigrations', 'avg_total_population', 'death', 'femenine_jan','jan' ]
    numerical_e = ['emigrations', 'avg_total_population', 'death', 'femenine_jan','jan' ]
    
    categorical = ['year','age', 'sex',
                       'reporting_country', 'reporting_country_name', 'reporting_country_region','reporting_country_sub-region', 
                      'country_birth','country_birth_name','country_birth_region', 'country_birth_sub-region']
    
    # convierto las columnas numéricas a tipo numérico
    for i in numerical_i:
        merged_data_inmi[i] = pd.to_numeric(merged_data_inmi[i], errors='coerce')
    
    for i in numerical_e:
        merged_data_emig[i] = pd.to_numeric(merged_data_emig[i], errors='coerce')
    
    # convierto las categoricas a category
    for i in categorical:
        merged_data_inmi[i] = merged_data_inmi[i].astype('category')
        merged_data_emig[i] = merged_data_emig[i].astype('category')
    
    In [16]:
    #numero de filas y atributos
    print("Shape of data (número de filas y atributos):", merged_data_inmi.shape , "\n")
    
    print("Shape of data (número de filas y atributos):", merged_data_emig.shape , "\n")
    
    #verifico si hay valores nulos
    # Verificar si todas las columnas tienen valores nulos
    nulos = merged_data_inmi.isna().any().any()  # Usa .all() dos veces
    
    if nulos:
        print("Algunos valores en el dataset inmigración son nulos")
        # Mostrar las columnas que contienen valores nulos
        columnas_con_nulos = merged_data_inmi.columns[merged_data_inmi.isna().any()]
        print("Columnas con valores nulos en inmig:", columnas_con_nulos)
    else:
        print("Ningun valor en el dataset inmigración son nulos")
        
    
    nulos = merged_data_emig.isna().any().any()  # Usa .all() dos veces
    
    if nulos:
        print("Algunos valores en el dataset emigracion son nulos")
        # Mostrar las columnas que contienen valores nulos
        columnas_con_nulos = merged_data_emig.columns[merged_data_emig.isna().any()]
        print("Columnas con valores nulos en emig:", columnas_con_nulos)
    
    else:
        print("Ningun valor en el dataset emigracion son nulos")
    
        
    
    Shape of data (número de filas y atributos): (742776, 16) 
    
    Shape of data (número de filas y atributos): (443912, 16) 
    
    Ningun valor en el dataset inmigración son nulos
    Ningun valor en el dataset emigracion son nulos
    

    Tenemos 251 países de nacimiento de 12 regiones en la inmigración internacional a 27 países europeos. En la emigración son 249 países también de 12 regiones y 13 países europeos reportando.

    In [43]:
    # Guardar los DataFrames combinados en archivos Excel para poder usarlos en otros sistemas de visualizacion
    merged_data_inmi.to_excel(os.path.join(carpeta, "Population_inmigration_indicators.xlsx"), index=False)
    merged_data_emig.to_excel(os.path.join(carpeta, "Population_emigration_indicators.xlsx"), index=False)
    

    2. Análisis de los datos¶

    2.1 Análisis estadístico básico¶

    • Variables categóricas:
      • Calculo la frecuencia.
      • Haced un gráfico de barras.
    • Variables numéricas:
      • Calculo estadísticos descriptivos básicos: media, mediana, desviación estandard, ...
      • Visualización distribucion
    In [17]:
    merged_data_inmi.describe
    merged_data_emig.describe
    
    #creamos variables con los nombres de las variables categóricas y numéricas para inmigracion
    tipos_de_datos_i = merged_data_inmi.dtypes
    variables_categoricas_i = tipos_de_datos_i[tipos_de_datos_i == 'category'].index
    variables_numericas_i = tipos_de_datos_i[tipos_de_datos_i != 'category'].index
    
    #creamos variables con los nombres de las variables categóricas y numéricas para emigracion
    tipos_de_datos_e = merged_data_emig.dtypes
    variables_categoricas_e = tipos_de_datos_e[tipos_de_datos_e == 'category'].index
    variables_numericas_e = tipos_de_datos_e[tipos_de_datos_e != 'category'].index
    
    print('Categoricas en inmigracion: ',variables_categoricas_i)
    print('Categoricas en emigracion: ',variables_categoricas_e)
    print('Numericas en inmigracion: ',variables_numericas_i)
    print('Numericas en emigracion: ',variables_numericas_e)
    
    Categoricas en inmigracion:  Index(['year', 'age', 'sex', 'reporting_country', 'reporting_country_name',
           'reporting_country_region', 'reporting_country_sub-region',
           'country_birth', 'country_birth_name', 'country_birth_region',
           'country_birth_sub-region'],
          dtype='object')
    Categoricas en emigracion:  Index(['year', 'age', 'sex', 'reporting_country', 'reporting_country_name',
           'reporting_country_region', 'reporting_country_sub-region',
           'country_birth', 'country_birth_name', 'country_birth_region',
           'country_birth_sub-region'],
          dtype='object')
    Numericas en inmigracion:  Index(['inmigrations', 'avg_total_population', 'death', 'femenine_jan', 'jan'], dtype='object')
    Numericas en emigracion:  Index(['emigrations', 'avg_total_population', 'death', 'femenine_jan', 'jan'], dtype='object')
    
    In [82]:
    #para las variables categóricas contamos las observaciones de cada grupo
    import matplotlib.pyplot as plt
    import seaborn as sns
    
    # Crear el gráfico
    for variable in variables_categoricas_i:
        # Contar las observaciones de cada grupo (categoría)
        counts = merged_data_inmi[variable].value_counts()
        
        # Crear el gráfico de barras horizontal
        plt.figure(figsize=(10, 6))  # Tamaño del gráfico
        sns.barplot(x=counts.values, y=counts.index, orient='h', palette='viridis')  # gráfico horizontal
        plt.title(f'Conteo de Observaciones inmigracion para {variable}')
        plt.xlabel('Número de Observaciones inmigracion')
        plt.ylabel('Categorías inmigracion')
        plt.show()
    
    # Crear el gráfico
    for variable in variables_categoricas_e:
        # Contar las observaciones de cada grupo (categoría)
        counts = merged_data_emig[variable].value_counts()
        
        # Crear el gráfico de barras horizontal
        plt.figure(figsize=(10, 6))  # Tamaño del gráfico
        sns.barplot(x=counts.values, y=counts.index, orient='h', palette='viridis')  # gráfico horizontal
        plt.title(f'Conteo de Observaciones emigracion para {variable}')
        plt.xlabel('Número de Observaciones emigracion')
        plt.ylabel('Categorías emigracion')
        plt.show()
    
    In [ ]:
    #quitamos los datos totales del dataframe
    filtered_data_inmi_wo_total = merged_data_inmi[(merged_data_inmi['age'] != 'TOTAL') & 
                                                   (merged_data_inmi['sex'] != 'TOTAL') & 
                                                   (merged_data_inmi['country_birth'] != 'TOTAL')]
    
    
    filtered_data_emig_wo_total = merged_data_emig[(merged_data_emig['age'] != 'TOTAL') & 
                                                   (merged_data_emig['sex'] != 'TOTAL') & 
                                                   (merged_data_emig['country_birth'] != 'TOTAL')]
    
    In [72]:
    #Estadísticos de las variables numéricas
    filtered_data_inmi_wo_total.describe().T
    
    Out[72]:
    count mean std min 25% 50% 75% max
    inmigrations 651808.0 1.808839e+02 3.978568e+03 0.0 0.0 0.0 5.000000e+00 1.255690e+06
    avg_total_population 651808.0 1.696725e+07 2.095429e+07 37494.0 2842639.0 5276968.0 1.744150e+07 8.379798e+07
    death 651808.0 1.827221e+05 2.287667e+05 249.0 41106.0 57023.0 1.686780e+05 1.066341e+06
    femenine_jan 651808.0 8.675940e+06 1.074500e+07 18813.0 1544399.0 2609187.0 8.759554e+06 4.217034e+07
    jan 651808.0 1.696073e+07 2.095909e+07 37366.0 2859077.0 5258317.0 1.740758e+07 8.323712e+07
    inmigrations_log 651808.0 1.186878e+00 1.947274e+00 0.0 0.0 0.0 1.791759e+00 1.404320e+01
    In [73]:
    filtered_data_emig_wo_total.describe().T
    
    Out[73]:
    count mean std min 25% 50% 75% max
    emigrations 370752.0 7.092228e+01 8.935490e+02 0.0 0.0 0.0 2.000000e+00 7.247800e+04
    avg_total_population 370752.0 2.300096e+07 2.664778e+07 2063531.0 2112076.0 2877325.0 5.913317e+07 6.022960e+07
    death 370752.0 2.529519e+05 2.934875e+05 19689.0 24016.0 41776.0 6.331330e+05 7.403170e+05
    femenine_jan 370752.0 1.181137e+07 1.367505e+07 1039839.0 1049485.0 1563839.0 3.036999e+07 3.106718e+07
    jan 370752.0 2.300844e+07 2.666255e+07 2062874.0 2108977.0 2895573.0 5.923621e+07 6.029550e+07
    emigrations_log 370752.0 8.968855e-01 1.692514e+00 0.0 0.0 0.0 1.098612e+00 1.119105e+01
    In [74]:
    #bloxplot para visualizar los estadísticos
    plt.figure(figsize=(10,15))
    
    # Un boxplot para cada variable numérica
    for i, col in enumerate(variables_numericas_e):
        plt.subplot(5,3,i+1)
        filtered_data_emig_wo_total.boxplot(col)
        plt.grid()
        plt.tight_layout()
    
    In [75]:
    #bloxplot para visualizar los estadísticos
    plt.figure(figsize=(10,15))
    
    # Un boxplot para cada variable numérica
    for i, col in enumerate(variables_numericas_i):
        plt.subplot(5,3,i+1)
        filtered_data_inmi_wo_total.boxplot(col)
        plt.grid()
        plt.tight_layout()
    
    In [125]:
    # Transformación logarítmica de la medida debido a lo dispersos que están los valores
    filtered_data_inmi_wo_total['inmigrations_log'] = np.log1p(filtered_data_inmi_wo_total['inmigrations'])  # log(1 + x) para evitar log(0)
    filtered_data_emig_wo_total['emigrations_log'] = np.log1p(filtered_data_emig_wo_total['emigrations'])
    
    # Ver los primeros registros después de la transformación
    print(filtered_data_inmi_wo_total[['inmigrations', 'inmigrations_log']].head())
    print(filtered_data_emig_wo_total[['emigrations', 'emigrations_log']].head())
    
          inmigrations  inmigrations_log
    1494             0          0.000000
    1495             0          0.000000
    1496             0          0.000000
    1497             0          0.000000
    1498             2          1.098612
           emigrations  emigrations_log
    13182            0         0.000000
    13183            0         0.000000
    13184            0         0.000000
    13185            3         1.386294
    13186            2         1.098612
    
    C:\Users\34617\AppData\Local\Temp\ipykernel_19920\4165713483.py:2: SettingWithCopyWarning:
    
    
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead
    
    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
    
    C:\Users\34617\AppData\Local\Temp\ipykernel_19920\4165713483.py:3: SettingWithCopyWarning:
    
    
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead
    
    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
    
    
    In [79]:
    import matplotlib.pyplot as plt
    import seaborn as sns
    
    # Histograma con escala logarítmica
    plt.figure(figsize=(10, 6))
    sns.histplot(filtered_data_inmi_wo_total['inmigrations_log'], kde=True)
    plt.yscale('log')  # Escala logarítmica para el eje y
    plt.xlabel('Inmigraciones (log transformadas)')
    plt.ylabel('Frecuencia')
    plt.title('Distribución de Inmigraciones (Escala Logarítmica)')
    plt.show()
    

    Hemos tenido que hacer un histograma usando la funcion logarítmica pues como vemos en los boxplots la distribución no es en absoluto normal y existen muchos outliers que dificulta la visualización del histograma. Vemos que hay una asimetría a la izquierda y que más del 75% de las observaciones tiene valores de inmigración inferiores a 10.

    In [80]:
    plt.figure(figsize=(10, 6))
    sns.boxplot(x=filtered_data_inmi_wo_total['inmigrations_log'])
    plt.title('Boxplot de Inmigraciones (Transformación Logarítmica)')
    plt.xlabel('Inmigraciones')
    plt.show()
    

    Vemos más claramente ahora un boxplot con el valor escalado de inmigraciones que hay muchos outliers. Realmente esto es interesante en esta visualización. Detectar estas anomalías o cuales son los factores geográficos o de edad que hacen que en algunos casos hay muchas más inmigraciones. Veamos cual sería la distribución eliminando del dataset los valores por debajo o igual a 10 en inmigraciones.

    In [124]:
    #filtramos por encima del cuartile 75% que nos daba 9 como valor en inmigraciones y 4 en emigraciones
    
    filtered_data_inmi = filtered_data_inmi_wo_total[filtered_data_inmi_wo_total['inmigrations'] > 5]
    filtered_data_emig = filtered_data_emig_wo_total[filtered_data_emig_wo_total['emigrations'] > 2]
    
    plt.figure(figsize=(10, 6))
    sns.histplot(filtered_data_inmi['inmigrations'], kde=False, bins=50)
    plt.yscale('log')
    plt.xlabel('Inmigraciones (>9)')
    plt.ylabel('Frecuencia')
    plt.title('Distribución de Inmigraciones (filtradas)')
    plt.show()
    
    In [82]:
    import seaborn as sns
    
    # Density plot 
    sns.kdeplot(filtered_data_inmi['inmigrations'])
    
    Out[82]:
    <AxesSubplot: xlabel='inmigrations', ylabel='Density'>
    In [129]:
    filtered_data_inmi.describe().T
    
    Out[129]:
    count mean std min 25% 50% 75% max
    inmigrations 158235.0 7.436048e+02 8.048951e+03 6.00000 1.400000e+01 4.100000e+01 1.850000e+02 1.255690e+06
    avg_total_population 158235.0 2.670599e+07 2.307056e+07 37494.00000 5.408320e+06 1.723162e+07 4.744382e+07 8.379798e+07
    death 158235.0 2.768515e+05 2.519286e+05 249.00000 5.697900e+04 1.518850e+05 4.619540e+05 1.066341e+06
    femenine_jan 158235.0 1.361830e+07 1.183113e+07 18813.00000 2.672110e+06 8.654043e+06 2.415219e+07 4.217034e+07
    jan 158235.0 2.667996e+07 2.307006e+07 37366.00000 5.391369e+06 1.718108e+07 4.740080e+07 8.323712e+07
    inmigrations_log 158235.0 4.151503e+00 1.811703e+00 1.94591 2.708050e+00 3.737670e+00 5.225747e+00 1.404320e+01

    El histograma filtrado ahora puede visualizarse correctamente y nos muestra aún asimetría a la izquierda es decir gran concentración de observaciones con pocas inmigraciones y además es un histograma con ciertos intervalos vacíos o histograma discontinuo.

    In [84]:
    plt.figure(figsize=(10, 6))
    sns.histplot(filtered_data_emig['emigrations'], kde=False, bins=50)
    plt.yscale('log')
    plt.xlabel('Inmigraciones (>4)')
    plt.ylabel('Frecuencia')
    plt.title('Distribución de Emigraciones (filtradas)')
    plt.show()
    
    In [42]:
    sns.kdeplot(filtered_data_emig['emigrations'])
    
    Out[42]:
    <AxesSubplot: xlabel='emigrations', ylabel='Density'>

    El histograma filtrado de emigraciones igual que el de inmigraciones nos muestra aún asimetría a la izquierda es decir gran concentración de observaciones con pocas emigraciones y además es un histograma con ciertos intervalos vacíos o histograma discontinuo

    In [37]:
    # visualicemos la poblacin y muertes
    # elimino duplicados basados en las columnas 'year', 'reporting_country'/'country_birth' 'deaths', y 'jan' pues la granularidad es diferente
    
    merged_data_unique_i = merged_data_inmi.drop_duplicates(subset=['year', 'reporting_country', 'country_birth','death', 'jan'])
    merged_data_unique_e = merged_data_emig.drop_duplicates(subset=['year', 'reporting_country', 'country_birth','death', 'jan'])
    
    
    #print(merged_data_unique_i.head())
    #print(merged_data_unique_e.head())
    
    histogramas = ['death', 'jan']
    
    fig, axs = plt.subplots(1, len(histogramas), figsize=(15, 5))
    
    # Histograma para cada valor en la variable histogramas
    for i, col in enumerate(histogramas):
        axs[i].hist(merged_data_unique_i[col])
        axs[i].set_title(f'Histograma inmi de {col}')
    
    # Superponemos
    plt.tight_layout()
    
    # Y pintamos
    plt.show()
    
    In [38]:
    # Histograma para cada valor en la variable histogramas
    fig, axs = plt.subplots(1, len(histogramas), figsize=(15, 5))
    
    for i, col in enumerate(histogramas):
        axs[i].hist(merged_data_unique_e[col])
        axs[i].set_title(f'Histograma emig de {col}')
    
    # Superponemos
    plt.tight_layout()
    
    # Y pintamos
    plt.show()
    

    La distribución de la población (jan) y muertes (death) también es asimétrica a la izquierda y son coherentes los números entre ambas. También los rangos son discontinuas presentando huecos en el histograma.

    2.2 Análisis exploratorio de los datos¶

    Exploramos la relación de algunos de los atributos numéricos con las variables categóricas.

    In [61]:
    import pandas as pd
    import plotly.express as px
    
    # Crear gráfico de barras horizontales
    fig = px.bar(
        merged_data_unique_i,
        x='jan',                     # Eje X: número de registros
        y='reporting_country',         # Eje Y: países
        orientation='h',               # Orientación horizontal
        title="Ranking de población por 'reporting_country'",
        labels={'jan': 'Población', 'reporting_country': 'País'},  # Etiquetas
        template="plotly_white"  ,      # Estilo del gráfico
        category_orders={'reporting_country': merged_data_unique_i.sort_values('jan', ascending=False)['reporting_country'].tolist()}
    )
    
    # Ajustar el eje Y para que se muestren todas las etiquetas
    fig.update_layout(
        yaxis=dict(
            tickmode='array',                     # Asegura que todas las categorías se muestren
            tickvals=merged_data_unique_i['reporting_country'].tolist(),  # Usa todas las categorías
            ticktext=merged_data_unique_i['reporting_country'].tolist(), # Muestra las etiquetas completas
            tickangle=0,                          # Para evitar que las etiquetas se corten
        ),
        height=600,  # Ajusta el tamaño del gráfico si es necesario
        width=1000   # Ajusta el ancho si es necesario
    )
    
    
    # Mostrar el gráfico
    fig.show()
    
    In [60]:
    import pandas as pd
    import plotly.express as px
    
    # Crear gráfico de barras horizontales
    fig = px.bar(
        merged_data_unique_i,
        x='death',                     # Eje X: número de registros
        y='reporting_country',         # Eje Y: países
        orientation='h',               # Orientación horizontal
        title="Ranking de Muertes por 'reporting_country'",
        labels={'death': 'Muertes', 'reporting_country': 'País'},  # Etiquetas
        template="plotly_white",        # Estilo del gráfico
        category_orders={'reporting_country': merged_data_unique_i.sort_values('jan', ascending=False)['reporting_country'].tolist()}
    
    )
    
    # Ajustar el eje Y para que se muestren todas las etiquetas
    fig.update_layout(
        yaxis=dict(
            tickmode='array',                     # Asegura que todas las categorías se muestren
            tickvals=merged_data_unique_i['reporting_country'].tolist(),  # Usa todas las categorías
            ticktext=merged_data_unique_i['reporting_country'].tolist(), # Muestra las etiquetas completas
            tickangle=0,                          # Para evitar que las etiquetas se corten
        ),
        height=600,  # Ajusta el tamaño del gráfico si es necesario
        width=1000   # Ajusta el ancho si es necesario
    )
    
    
    # Mostrar el gráfico
    fig.show()
    
    In [64]:
    # Calcular la tasa de muertes por población (por cada 100,000 habitantes)
    merged_data_unique_i['death_rate_per_100k'] = (merged_data_unique_i['death'] / merged_data_unique_i['jan']) * 100000
    
    # Agrupar por 'reporting_country' y calcular el promedio de la tasa de muertes por población
    data_aggregated = merged_data_unique_i.groupby('reporting_country_name')['death_rate_per_100k'].mean().reset_index()
    
    # Ordenar los datos de mayor a menor
    data_aggregated = data_aggregated.sort_values(by='death_rate_per_100k', ascending=False)
    
    # Crear gráfico de barras horizontales
    fig = px.bar(
        data_aggregated,
        x='death_rate_per_100k',         # Eje X: tasa de muertes por población
        y='reporting_country_name',           # Eje Y: países
        orientation='h',                 # Orientación horizontal
        title="Tasa de muertes por población por 'reporting_country' (Ordenado de Mayor a Menor)",
        labels={'death_rate_per_100k': 'Tasa de muertes por cada 100,000 habitantes', 'reporting_country_name': 'País'},  # Etiquetas
        template="plotly_white",         # Estilo del gráfico
        category_orders={'reporting_country_name': data_aggregated.sort_values('death_rate_per_100k', ascending=False)['reporting_country_name'].tolist()}
    )
    
    # Ajustar el eje Y para que se muestren todas las etiquetas
    fig.update_layout(
        yaxis=dict(
            tickmode='array',                     # Asegura que todas las categorías se muestren
            tickvals=data_aggregated['reporting_country_name'].tolist(),  # Usa todas las categorías
            ticktext=data_aggregated['reporting_country_name'].tolist(), # Muestra las etiquetas completas
            tickangle=0,                          # Para evitar que las etiquetas se corten
        ),
        height=600,  # Ajusta el tamaño del gráfico si es necesario
        width=1000   # Ajusta el ancho si es necesario
    )
    
    # Mostrar el gráfico
    fig.show()
    
    In [86]:
    import pandas as pd
    import numpy as np
    import plotly.graph_objects as go
    from scipy.stats import gaussian_kde
    
    
    # Configuración inicial
    countries = filtered_data_inmi['country_birth_sub-region'].unique()  # Usamos filtered_data_inmi para las subregiones
    fig = go.Figure()
    
    # Recorremos cada subregión (country_birth_sub-region) y creamos una curva KDE desplazada
    for i, country in enumerate(countries):
        # Subconjunto de datos para cada país, filtrado por 'country_birth_sub-region' en filtered_data_inmi
        subset = merged_data_unique_i[merged_data_unique_i['country_birth_sub-region'] == country]['inmigrations']
        
        # Verificar que subset no esté vacío y tenga suficiente variabilidad
        if subset.empty or subset.nunique() == 1:  # Si solo hay un valor único, omitir este país
            continue
    
        # Calcular la densidad KDE
        try:
            kde = gaussian_kde(subset)
            x_vals = np.linspace(subset.min(), subset.max(), 500)  # Generar un rango de valores para evaluar KDE
            y_vals = kde(x_vals)
            
            # Desplazar cada curva verticalmente para que no se solapen
            y_vals_shifted = y_vals + i * 0.2  # Ajusta el desplazamiento vertical aquí
    
            # Agregar la curva al gráfico
            fig.add_trace(go.Scatter(
                x=x_vals, 
                y=y_vals_shifted, 
                mode='lines', 
                fill='tonexty',  # Rellenar debajo de la curva
                name=country  # Nombre de la subregión
            ))
        except Exception as e:
            print(f"Error al calcular KDE para {country}: {e}")
            continue
    
    # Personalización del gráfico
    fig.update_layout(
        title="Ridgeline Plot de inmigraciones por 'country_birth_sub-region'",
        xaxis_title="Inmigraciones",  
        yaxis=dict(
            tickvals=[i * 0.2 for i in range(len(countries))],  # Posición en el eje y para cada curva
            ticktext=countries,  # Etiquetas para cada subregión
            showgrid=False,  # No mostrar la cuadrícula en el eje Y
        ),
        showlegend=False,  # No mostrar la leyenda
        template="plotly_white",
        height=600,
        width=1000
    )
    
    fig.show()
    
    In [87]:
    # getting necessary libraries
    import plotly.graph_objects as go
    import numpy as np
    import pandas as pd
    
    # getting the data
    temp = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/2016-weather-data-seattle.csv') # we retrieve the data from plotly's GitHub repository
    temp['year'] = pd.to_datetime(temp['Date']).dt.year # we store the year in a separate column
    
    # Since we do not want to plot 50+ lines, we only select some years to plot
    year_list = [1950, 1960, 1970, 1980, 1990, 2000, 2010]
    temp = temp[temp['year'].isin(year_list)]
    
    # as we expect to plot histograms-like plots for each year, we group by year and mean temperature and aggregate with 'count' function
    temp = temp.groupby(['year', 'Mean_TemperatureC']).agg({'Mean_TemperatureC': 'count'}).rename(columns={'Mean_TemperatureC': 'count'}).reset_index()
    
    
    # the idea behind this ridgeline plot with Plotly is to add traces manually, each trace corresponding to a particular year's temperature distribution
    # thus, we are to store each year's data (temperatures and their respective count) in seperate arrays or pd.series that we store in a dictionnary to retrieve them easily
    array_dict = {} # instantiating an empty dictionnary
    for year in year_list:
        array_dict[f'x_{year}'] = temp[temp['year']==year]['Mean_TemperatureC'] # storing the temperature data for each year
        array_dict[f'y_{year}'] = temp[temp['year']==year]['count'] # storing the temperature count for each year
        array_dict[f'y_{year}'] = (array_dict[f'y_{year}'] - array_dict[f'y_{year}'].min()) \
                                    / (array_dict[f'y_{year}'].max() - array_dict[f'y_{year}'].min()) # we normalize the array (min max normalization)
    
    # once all of this is done, we can create a plotly.graph_objects.Figure and add traces with fig.add_trace() method
    # since we have stored the temperatures and their respective count for each year, we can plot scatterplots (go.Scatter)
    # we thus iterate over year_list and create a 'blank line' that is placed at y = index, then the corresponding temperature count line
    fig = go.Figure()
    for index, year in enumerate(year_list):
        fig.add_trace(go.Scatter(
                                x=[-20, 40], y=np.full(2, len(year_list)-index),
                                mode='lines',
                                line_color='white'))
        
        fig.add_trace(go.Scatter(
                                x=array_dict[f'x_{year}'],
                                y=array_dict[f'y_{year}'] + (len(year_list)-index) + 0.4,
                                fill='tonexty',
                                name=f'{year}'))
        
        # plotly.graph_objects' way of adding text to a figure
        fig.add_annotation(
                            x=-20,
                            y=len(year_list)-index,
                            text=f'{year}',
                            showarrow=False,
                            yshift=10)
    
    # here you can modify the figure and the legend titles
    fig.update_layout(
                    title='Average temperature from 1950 until 2010 in Seattle',
                    showlegend=False,
                    xaxis=dict(title='Temperature in degree Celsius'),
                    yaxis=dict(showticklabels=False) # that way you hide the y axis ticks labels
                    )
    
    fig.show()
    
    In [96]:
    import plotly.graph_objects as go
    import numpy as np
    import pandas as pd
    from scipy.stats import gaussian_kde
    
    # Configuración inicial
    countries = filtered_data_inmi['country_birth_sub-region'].unique()  # Usamos filtered_data_inmi para las subregiones
    
    # Filtramos los datos para las subregiones y eliminamos valores nulos o extremos
    filtered_data_inmi_bycb = filtered_data_inmi[filtered_data_inmi['country_birth_sub-region'].isin(countries)]
    
    # Aseguramos que no tengamos valores cero antes de aplicar el logaritmo
    filtered_data_inmi_bycb = filtered_data_inmi_bycb[filtered_data_inmi_bycb['inmigrations'] > 0]  
    
    # Creamos una nueva columna con el logaritmo de las inmigraciones
    filtered_data_inmi_bycb['inmigrations_log'] = np.log(filtered_data_inmi_bycb['inmigrations'])
    
    # Diccionario para almacenar los datos de inmigración logarítmica
    array_dict = {}
    
    for country in countries:
        # Extraemos los datos correspondientes para cada subregión
        subset = filtered_data_inmi_bycb[filtered_data_inmi_bycb['country_birth_sub-region'] == country]
        
        # Realizamos una estimación de densidad con KDE para suavizar las distribuciones
        kde = gaussian_kde(subset['inmigrations_log'], bw_method=0.2)  # Ajustamos el parámetro de ancho de banda si es necesario
        x_vals = np.linspace(subset['inmigrations_log'].min(), subset['inmigrations_log'].max(), 500)  # Rango de valores logarítmicos
        y_vals = kde(x_vals)  # Evaluamos la densidad
        
        # Normalizamos los valores para que las curvas no se solapen
        y_vals_normalized = (y_vals - y_vals.min()) / (y_vals.max() - y_vals.min())  # Normalización
    
        # Guardamos los valores de x e y para cada país
        array_dict[f'x_{country}'] = x_vals
        array_dict[f'y_{country}'] = y_vals_normalized + (len(countries)-np.where(countries == country)[0][0])  # Desplazamos las curvas
    
    # Creamos la figura de Plotly
    fig = go.Figure()
    
    # Añadimos las curvas de cada país
    for index, country in enumerate(countries):
        # Añadimos una línea base blanca para separar las curvas
        fig.add_trace(go.Scatter(
                                x=[-20, 40], y=np.full(2, len(countries)-index),
                                mode='lines',
                                line_color='white'))
        
        # Añadimos la curva de inmigración logarítmica con su correspondiente relleno
        fig.add_trace(go.Scatter(
                                x=array_dict[f'x_{country}'],
                                y=array_dict[f'y_{country}'],
                                fill='tonexty',
                                name=f'{country}'))
        
        # Añadimos el nombre del país como anotación (fuera del gráfico para evitar solapamiento)
        fig.add_annotation(
                            x=array_dict[f'x_{country}'][0],  # Primer valor de 'x' de la curva
                            y=len(countries)-index + 0.3,  # Ajustamos la posición para que no se solape
                            text=f'{country}',
                            showarrow=False,
                            font=dict(size=10),
                            yshift=10)
    
    # Personalización del gráfico
    fig.update_layout(
                    title='Distribución por país de nacimiento de inmigraciones (escala logarítmica)',
                    showlegend=False,
                    xaxis=dict(title='Inmigraciones (log)'),
                    yaxis=dict(showticklabels=False),  # Ocultamos las etiquetas del eje Y
                    template="plotly_white",  # Estilo blanco para el gráfico
                    height=800,
                    width=1000
                    )
    
    fig.show()
    

    Vemos claramente con el ridline por subregiones una asimetría a la derecha y valores outlier en Central Africa, Caribbean y Easter Europe. Más pronunciada en las dos primeras.

    In [102]:
    import plotly.graph_objects as go
    import pandas as pd
    
    
    # datos agrupados por subregión y año
    filtered_data_inmi_grouped = filtered_data_inmi.groupby(['country_birth_sub-region', 'year']).agg({'inmigrations_log': 'sum'}).reset_index()
    
    # una figura de Plotly
    fig = go.Figure()
    
    # todas las subregiones únicas
    subregions = filtered_data_inmi_grouped['country_birth_sub-region'].unique()
    
    # Definiré una paleta de colores para daltonismo se haría de este modo aprox.
    colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
    
    # una línea para cada subregión con colores definidos arriba
    for i, subregion in enumerate(subregions):
        # Filtro los datos por subregión
        subregion_data = filtered_data_inmi_grouped[filtered_data_inmi_grouped['country_birth_sub-region'] == subregion]
        
        # un color de la paleta a la subregión
        color = colors[i % len(colors)]  # Aseguramos así que no se repitan colores si hay más subregiones que colores
        
        # Añado la línea para esa subregión
        fig.add_trace(go.Scatter(
            x=subregion_data['year'], 
            y=subregion_data['inmigrations_log'], 
            mode='lines+markers', 
            name=subregion,
            line=dict(color=color, width=2),  # Ajustar el grosor de la línea
            marker=dict(size=8)  # Tamaño de los puntos
        ))
    
    # Personalizacion del gráfico
    fig.update_layout(
        title='Evolución de las Inmigraciones por Subregión a lo largo del tiempo',
        xaxis_title='Año',
        yaxis_title='Inmigraciones (Log)',
        template='plotly_white',
        showlegend=True,
        height=600,
        width=1000
    )
    
    # Mostramos el gráfico
    fig.show()
    
    In [149]:
    # Miramos ese pico alto entre 2021 y 2022 en Eastern Europe en más detalle -> drill down
    
    import plotly.graph_objects as go
    
    # Agrupar los datos por 'country_birth_sub-region', 'country_birth_name' y 'year'
    filtered_data_inmi_grouped = filtered_data_inmi.groupby(['country_birth_sub-region','country_birth_name', 'year']).agg({'inmigrations_log': 'sum'}).reset_index()
    
    # Filtrar los datos para Eastern Europe
    filtered_data_inmi_grouped_EAE = filtered_data_inmi_grouped[filtered_data_inmi_grouped['country_birth_sub-region'] == 'Eastern Europe']
    
    filtered_data_inmi_grouped_EAE.loc[:, 'country_birth_name'] = filtered_data_inmi_grouped_EAE['country_birth_name'].astype(str)
    
    # Crear la figura
    fig = go.Figure()
    
    #filtered_data_inmi_grouped_EAE = merged_data_inmi[merged_data_inmi['country_birth_sub-region'] == 'Eastern Europe']
    countries_EAE = filtered_data_inmi_grouped_EAE['country_birth_name'].astype(str).unique()
    
    print("Países de Eastern Europe:", countries_EAE)
    
    # Iterar por cada país en Eastern Europe y agregar una línea al gráfico
    countries = filtered_data_inmi_grouped_EAE['country_birth_name'].astype(str).unique()
    
    print(countries)
    
    for country in countries:
        # Filtrar los datos para el país actual
        country_data = filtered_data_inmi_grouped_EAE[filtered_data_inmi_grouped_EAE['country_birth_name'] == country]
        
        # Agregar la traza de la línea para este país
        fig.add_trace(go.Scatter(
            x=country_data['year'],  # Eje X: años
            y=country_data['inmigrations_log'],  # Eje Y: inmigraciones (log)
            mode='lines+markers',  # Usar líneas y marcadores
            name=country  # Nombre de la traza es el país
        ))
    
    # Personalización del gráfico
    fig.update_layout(
        title='Inmigraciones por país de nacimiento en Eastern Europe',
        xaxis=dict(
            title='Año',
            tickmode='linear',  # Asegurar que los años estén distribuidos linealmente
            dtick=1  # Mostrar todos los años
        ),
        yaxis=dict(
            title='Inmigraciones (log)',
            rangemode='tozero'  # Asegurar que el eje Y comience en 0
        ),
        legend_title='País de Nacimiento',
        template='plotly_white',  # Estilo de gráfico blanco
        height=600,  # Ajuste de tamaño
        width=1000  # Ajuste de tamaño
    )
    
    # Mostrar el gráfico
    fig.show()
    
    C:\Users\34617\AppData\Local\Temp\ipykernel_19920\2517032948.py:11: SettingWithCopyWarning:
    
    
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead
    
    See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
    
    
    Países de Eastern Europe: ['Afghanistan' 'Africa' 'Albania' 'Algeria' 'America' 'Andorra' 'Angola'
     'Anguilla' 'Antarctica' 'Antigua and Barbuda' 'Argentina' 'Armenia'
     'Aruba' 'Asia' 'Australia' 'Australia y Nueva Zelanda' 'Austria'
     'Azerbaijan' 'Bahamas' 'Bahrain' 'Bangladesh' 'Barbados' 'Belarus'
     'Belgium' 'Belize' 'Benin' 'Bermuda' 'Bhutan'
     'Bolivia, Plurinational State of' 'Bosnia and Herzegovina' 'Botswana'
     'Brazil' 'Brunei Darussalam' 'Bulgaria' 'Burkina Faso' 'Burundi'
     'Cabo Verde' 'Cambodia' 'Cameroon' 'Canada' 'Caribbean' 'Cayman Islands'
     'Central Africa' 'Central African Republic' 'Central America'
     'Central Asia' 'Chad' 'Chequia and Slovaquia' 'Chile' 'China' 'Colombia'
     'Comoros' 'Congo' 'Congo, Democratic Republic of the' 'Costa Rica'
     'Croatia' 'Cuba' 'Cyprus' 'Czechia' "Côte d'Ivoire" 'Denmark' 'Djibouti'
     'Dominica' 'Dominican Republic' 'EFTA countries and others'
     'Eastern Africa' 'Eastern Asia' 'Ecuador' 'Egypt' 'El Salvador'
     'Equatorial Guinea' 'Eritrea' 'Estonia' 'Eswatini' 'Ethiopia' 'Europe'
     'Ex Soviet Union' 'Ex Unión Soviética' 'Falkland Islands (Malvinas)'
     'Faroe Islands' 'Fiji' 'Finland' 'France' 'French Polynesia' 'Gabon'
     'Gambia' 'Georgia' 'Germany' 'Ghana' 'Gibraltar' 'Greece' 'Greenland'
     'Grenada' 'Guatemala' 'Guernsey' 'Guinea' 'Guinea-Bissau' 'Guyana'
     'Haiti' 'Holy See' 'Honduras' 'Hungary' 'Iceland' 'India' 'Indonesia'
     'Iran, Islamic Republic of' 'Iraq' 'Ireland' 'Isle of Man' 'Israel'
     'Italy' 'Jamaica' 'Japan' 'Jersey' 'Jordan' 'Kazakhstan' 'Kenya'
     'Kiribati' "Korea, Democratic People's Republic of" 'Korea, Republic of'
     'Kosovo' 'Kuwait' 'Kyrgyzstan' "Lao People's Democratic Republic"
     'Latvia' 'Lebanon' 'Lesotho' 'Liberia' 'Libya' 'Liechtenstein'
     'Lithuania' 'Luxembourg' 'Madagascar' 'Malawi' 'Malaysia' 'Maldives'
     'Mali' 'Malta' 'Marshall Islands' 'Mauritania' 'Mauritius' 'Melanesia'
     'Mexico' 'Micronesia' 'Micronesia, Federated States of'
     'Moldova, Republic of' 'Monaco' 'Mongolia' 'Montenegro' 'Montserrat'
     'Morocco' 'Mozambique' 'Myanmar' 'Nauru' 'Nepal'
     'Netherlands, Kingdom of the' 'New Caledonia' 'New Zealand' 'Nicaragua'
     'Niger' 'Nigeria' 'North Macedonia' 'Northern Africa' 'Northern America'
     'Norway' 'Oceania' 'Oman' 'Other' 'Pakistan' 'Palau'
     'Palestine, State of' 'Panama' 'Papua New Guinea' 'Paraguay' 'Peru'
     'Philippines' 'Pitcairn' 'Poland' 'Portugal' 'Qatar' 'Romania'
     'Russian Federation' 'Rwanda' 'Saint Barthélemy'
     'Saint Helena, Ascension and Tristan da Cunha' 'Saint Kitts and Nevis'
     'Saint Lucia' 'Saint Pierre and Miquelon'
     'Saint Vincent and the Grenadines' 'Samoa' 'San Marino'
     'Sao Tome and Principe' 'Saudi Arabia' 'Senegal' 'Serbia'
     'Serbia and Montenegro' 'Seychelles' 'Sierra Leone' 'Singapore'
     'Slovakia' 'Slovenia' 'Solomon Islands' 'Somalia' 'South Africa'
     'South Sudan' 'Southeast Asia' 'Southern Africa' 'Southern America'
     'Southern Asia' 'Spain' 'Sri Lanka' 'Sudan' 'Suriname' 'Sweden'
     'Switzerland' 'Syrian Arab Republic' 'TOTAL' 'Taiwan'
     'Taiwan, Province of China' 'Tajikistan' 'Tanzania, United Republic of'
     'Thailand' 'Timor-Leste' 'Togo' 'Tonga' 'Trinidad and Tobago' 'Tunisia'
     'Turkmenistan' 'Turks and Caicos Islands' 'Tuvalu' 'Türkiye' 'UNKNOWN'
     'Uganda' 'Ukraine' 'United Arab Emirates'
     'United Kingdom of Great Britain and Northern Ireland'
     'United States of America' 'Uruguay' 'Uzbekistan' 'Vanuatu'
     'Venezuela, Bolivarian Republic of' 'Viet Nam' 'Virgin Islands (British)'
     'Wallis and Futuna' 'Western Africa' 'Western Asia' 'Western Sahara'
     'Yemen' 'Zambia' 'Zimbabwe']
    ['Afghanistan' 'Africa' 'Albania' 'Algeria' 'America' 'Andorra' 'Angola'
     'Anguilla' 'Antarctica' 'Antigua and Barbuda' 'Argentina' 'Armenia'
     'Aruba' 'Asia' 'Australia' 'Australia y Nueva Zelanda' 'Austria'
     'Azerbaijan' 'Bahamas' 'Bahrain' 'Bangladesh' 'Barbados' 'Belarus'
     'Belgium' 'Belize' 'Benin' 'Bermuda' 'Bhutan'
     'Bolivia, Plurinational State of' 'Bosnia and Herzegovina' 'Botswana'
     'Brazil' 'Brunei Darussalam' 'Bulgaria' 'Burkina Faso' 'Burundi'
     'Cabo Verde' 'Cambodia' 'Cameroon' 'Canada' 'Caribbean' 'Cayman Islands'
     'Central Africa' 'Central African Republic' 'Central America'
     'Central Asia' 'Chad' 'Chequia and Slovaquia' 'Chile' 'China' 'Colombia'
     'Comoros' 'Congo' 'Congo, Democratic Republic of the' 'Costa Rica'
     'Croatia' 'Cuba' 'Cyprus' 'Czechia' "Côte d'Ivoire" 'Denmark' 'Djibouti'
     'Dominica' 'Dominican Republic' 'EFTA countries and others'
     'Eastern Africa' 'Eastern Asia' 'Ecuador' 'Egypt' 'El Salvador'
     'Equatorial Guinea' 'Eritrea' 'Estonia' 'Eswatini' 'Ethiopia' 'Europe'
     'Ex Soviet Union' 'Ex Unión Soviética' 'Falkland Islands (Malvinas)'
     'Faroe Islands' 'Fiji' 'Finland' 'France' 'French Polynesia' 'Gabon'
     'Gambia' 'Georgia' 'Germany' 'Ghana' 'Gibraltar' 'Greece' 'Greenland'
     'Grenada' 'Guatemala' 'Guernsey' 'Guinea' 'Guinea-Bissau' 'Guyana'
     'Haiti' 'Holy See' 'Honduras' 'Hungary' 'Iceland' 'India' 'Indonesia'
     'Iran, Islamic Republic of' 'Iraq' 'Ireland' 'Isle of Man' 'Israel'
     'Italy' 'Jamaica' 'Japan' 'Jersey' 'Jordan' 'Kazakhstan' 'Kenya'
     'Kiribati' "Korea, Democratic People's Republic of" 'Korea, Republic of'
     'Kosovo' 'Kuwait' 'Kyrgyzstan' "Lao People's Democratic Republic"
     'Latvia' 'Lebanon' 'Lesotho' 'Liberia' 'Libya' 'Liechtenstein'
     'Lithuania' 'Luxembourg' 'Madagascar' 'Malawi' 'Malaysia' 'Maldives'
     'Mali' 'Malta' 'Marshall Islands' 'Mauritania' 'Mauritius' 'Melanesia'
     'Mexico' 'Micronesia' 'Micronesia, Federated States of'
     'Moldova, Republic of' 'Monaco' 'Mongolia' 'Montenegro' 'Montserrat'
     'Morocco' 'Mozambique' 'Myanmar' 'Nauru' 'Nepal'
     'Netherlands, Kingdom of the' 'New Caledonia' 'New Zealand' 'Nicaragua'
     'Niger' 'Nigeria' 'North Macedonia' 'Northern Africa' 'Northern America'
     'Norway' 'Oceania' 'Oman' 'Other' 'Pakistan' 'Palau'
     'Palestine, State of' 'Panama' 'Papua New Guinea' 'Paraguay' 'Peru'
     'Philippines' 'Pitcairn' 'Poland' 'Portugal' 'Qatar' 'Romania'
     'Russian Federation' 'Rwanda' 'Saint Barthélemy'
     'Saint Helena, Ascension and Tristan da Cunha' 'Saint Kitts and Nevis'
     'Saint Lucia' 'Saint Pierre and Miquelon'
     'Saint Vincent and the Grenadines' 'Samoa' 'San Marino'
     'Sao Tome and Principe' 'Saudi Arabia' 'Senegal' 'Serbia'
     'Serbia and Montenegro' 'Seychelles' 'Sierra Leone' 'Singapore'
     'Slovakia' 'Slovenia' 'Solomon Islands' 'Somalia' 'South Africa'
     'South Sudan' 'Southeast Asia' 'Southern Africa' 'Southern America'
     'Southern Asia' 'Spain' 'Sri Lanka' 'Sudan' 'Suriname' 'Sweden'
     'Switzerland' 'Syrian Arab Republic' 'TOTAL' 'Taiwan'
     'Taiwan, Province of China' 'Tajikistan' 'Tanzania, United Republic of'
     'Thailand' 'Timor-Leste' 'Togo' 'Tonga' 'Trinidad and Tobago' 'Tunisia'
     'Turkmenistan' 'Turks and Caicos Islands' 'Tuvalu' 'Türkiye' 'UNKNOWN'
     'Uganda' 'Ukraine' 'United Arab Emirates'
     'United Kingdom of Great Britain and Northern Ireland'
     'United States of America' 'Uruguay' 'Uzbekistan' 'Vanuatu'
     'Venezuela, Bolivarian Republic of' 'Viet Nam' 'Virgin Islands (British)'
     'Wallis and Futuna' 'Western Africa' 'Western Asia' 'Western Sahara'
     'Yemen' 'Zambia' 'Zimbabwe']
    
    In [103]:
    import plotly.graph_objects as go
    import pandas as pd
    
    
    # datos agrupados por subregión y año
    filtered_data_inmi_grouped = filtered_data_inmi.groupby(['reporting_country_name', 'year']).agg({'inmigrations_log': 'sum'}).reset_index()
    
    # una figura de Plotly
    fig = go.Figure()
    
    # todas las subregiones únicas
    subregions = filtered_data_inmi_grouped['reporting_country_name'].unique()
    
    # Definiré una paleta de colores para daltonismo se haría de este modo aprox.
    colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf']
    
    # una línea para cada subregión con colores definidos arriba
    for i, subregion in enumerate(subregions):
        # Filtro los datos por subregión
        subregion_data = filtered_data_inmi_grouped[filtered_data_inmi_grouped['reporting_country_name'] == subregion]
        
        # un color de la paleta a la subregión
        color = colors[i % len(colors)]  # Aseguramos así que no se repitan colores si hay más subregiones que colores
        
        # Añado la línea para esa subregión
        fig.add_trace(go.Scatter(
            x=subregion_data['year'], 
            y=subregion_data['inmigrations_log'], 
            mode='lines+markers', 
            name=subregion,
            line=dict(color=color, width=2),  # Ajustar el grosor de la línea
            marker=dict(size=8)  # Tamaño de los puntos
        ))
    
    # Personalizacion del gráfico
    fig.update_layout(
        title='Evolución de las Inmigraciones por Pais que reporta a lo largo del tiempo',
        xaxis_title='Año',
        yaxis_title='Inmigraciones (Log)',
        template='plotly_white',
        showlegend=True,
        height=600,
        width=1000
    )
    
    # Mostramos el gráfico
    fig.show()
    
    In [40]:
    import pandas as pd
    import seaborn as sns
    import matplotlib.pyplot as plt
    
    # Configuración de estilo
    sns.set_theme(style="whitegrid")
    
    # Filtrar o agrupar datos si es necesario (opcional)
    # Por ejemplo: merged_data_unique_i = merged_data_unique_i.dropna(subset=['jan', 'reporting_country'])
    
    # Crear el gráfico tipo ridge line
    plt.figure(figsize=(12, 8))
    sns.kdeplot(
        data=merged_data_unique_i,
        x="jan",
        hue="reporting_country",
        multiple="stack",  # Alternativamente: 'fill' o 'layer'
        palette="muted"
    )
    
    plt.title("Ridge Line Plot de la medida 'jan' por 'reporting_country'")
    plt.xlabel("jan")
    plt.ylabel("Densidad")
    plt.show()
    
    Análisis:
    Mirando los histogramas, ¿qué atributo parece tener más peso a la hora de clasificar un vino? ¿Cuál parece tener menos peso?

    Por los histogramas y sus superposiciones podemos ver la distinción de los tipos más claramente en el caso del análisis por la variable alcohol donde el tipo 1 al menos se distingue del resto, también en color_intensity sucede con el tipo 2.

    Ejercicio: Usando los histogramas anteriores, añadid una línea vertical indicando la media de cada uno de los histogramas (tres por gráfico). Pintad las líneas del mismo color que el histograma para que quede claro a cuál hacen referencia. Añadid a la leyenda la clase de vino y la desviación estándar en cuestión. La finalidad es verificar numéricamente las diferencias identificadas anteriormente de forma visual.
    Sugerencia: podeis usar "axvline", de matplotlib axis, para las líneas verticales.
    In [126]:
    class0 = dfwine[dfwine.target == 0]
    class1 = dfwine[dfwine.target == 1]
    class2 = dfwine[dfwine.target == 2]
    
    fig, axs = plt.subplots(1, len(feats_to_explore), figsize=(15, 5))
    
    # Un Histograma por variable
    for i, col in enumerate(feats_to_explore):
        axs[i].hist(class0[col], alpha=0.4, color='tab:blue', label='Tipo 0')
        axs[i].hist(class1[col], alpha=0.4, color='tab:olive', label='Tipo 1')
        axs[i].hist(class2[col], alpha=0.4, color='tab:orange', label='Tipo 2')
        axs[i].set_title(f'Histograma de {col}')
        axs[i].legend() # leyenda
        axs[i].axvline(class0[col].mean(), color='tab:blue', linestyle='dashed', linewidth=1)
        axs[i].axvline(class1[col].mean(), color='tab:olive', linestyle='dashed', linewidth=1)
        axs[i].axvline(class2[col].mean(), color='tab:orange', linestyle='dashed', linewidth=1)
        
    # No superponer los diferentes histogramas de variables diferentes
    plt.tight_layout()
    
    # Mostrar los histogramas
    plt.show()
    
    Ejercicio: Calculad y mostrad la correlación entre las tres variables que estamos analizando.
    In [127]:
    #calculamos la correlacion usando spearman
    corrmat = dfwine[feats_to_explore].corr(method='spearman')
    print(corrmat)
    
                      alcohol  magnesium  color_intensity
    alcohol          1.000000   0.365503         0.635425
    magnesium        0.365503   1.000000         0.357029
    color_intensity  0.635425   0.357029         1.000000
    
    In [128]:
    #visualizamos con un heatmap
    f, ax = plt.subplots(figsize=(5, 5))
    sns.heatmap(corrmat, ax=ax, cmap="YlGnBu", linewidths=0.1)
    
    Out[128]:
    <AxesSubplot:>
    Ejercicio: Representad gráficamente las relaciones entre estas variables (scatterplots). Diferenciad con colores diferentes las diferentes clases. La finalidad es poder observar y analizar las correlaciones de manera gráfica entre algunas de las variables.
    Sugerencia: podéis usar la función "pairplot" de la librería 'seaborn' con el parámetro "hue".
    In [129]:
    #visualicemos
    corrdf=dfwine[['alcohol','magnesium','color_intensity','target']]
    sns.pairplot(corrdf,hue='target')
    
    Out[129]:
    <seaborn.axisgrid.PairGrid at 0x14211ea4130>
    Ejercicio: Representad en 3D las tres variables. Poned nombres en los ejes y diferenciad con colores diferentes las diferentes clases de vino. La finalidad es complementar los gráficos anteriores y poder observar qué variables discriminan mejor entre las tres clases de vino.
    In [130]:
    from mpl_toolkits import mplot3d
    
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(221, projection='3d')
    alpha=0.4
    ax.scatter(corrdf['alcohol'], corrdf['magnesium'], corrdf['color_intensity'], c=corrdf['target'],cmap='viridis',  alpha=alpha)
    ax.set_xlabel('alcohol')
    ax.set_ylabel('magnesium')
    ax.set_zlabel('color')
    
    ax = fig.add_subplot(222)
    ax.scatter(corrdf['alcohol'], corrdf['magnesium'], c=corrdf['target'], cmap='viridis', alpha=alpha)
    ax.set_xlabel('alcohol')
    ax.set_ylabel('magnesium')
    
    ax = fig.add_subplot(223)
    ax.scatter(corrdf['alcohol'], corrdf['color_intensity'], c=corrdf['target'], cmap='viridis', alpha=alpha)
    ax.set_xlabel('alcohol')
    ax.set_ylabel('color')
     
    ax = fig.add_subplot(224)
    ax.scatter(corrdf['magnesium'], corrdf['color_intensity'], c=corrdf['target'], cmap='viridis', alpha=alpha)
    ax.set_xlabel('magnesium')
    ax.set_ylabel('color')
    
    Out[130]:
    Text(0, 0.5, 'color')
    Análisis:
    Observando las correlaciones, ¿qué variables son las que tienen una correlación más fuerte? ¿Cuadra el resultado numérico con los gráficos obtenidos?

    La correlación más fuerte se da entre color_intensity y alcohol de 0.63 y coincide con el gráfico de correlación donde se percibe una correlación positiva para los tres tipos de vino ya que los puntos siguen una distribución lineal que indica que cuando aumenta el alcohol aumenta el color_intensity. En el heatmap también vemos más obscuro coloreada la intersección entre estas dos variables.

    3. Preprocesado de los datos (2.5 puntos)¶

    Una vez analizados los atributos descriptivos, es el momento de prepararlos para que nos sean útiles de cara a predecir valores. En este apartado:

  • Estandarizaremos los valores de los atributos descriptivos para que sus escalas no sean muy diferentes.
  • Separaremos el conjunto de datos original en dos subconjuntos: entrenamiento y test.
  • Ejercicio: estandariza todos los atributos descriptivos.
    Sugerencia: utilizad "StandardScaler" de "preprocessing".
    In [131]:
    from sklearn.preprocessing import StandardScaler
    #variables descriptivas
    descriptivos = dfwine.iloc[:,:-1]
    #instanciamos standarscaler
    scaler = StandardScaler()
    #aplicamos la transformación
    descriptivos_escalados = scaler.fit_transform(descriptivos)
    #valores antes de reemplazar atributos en el dataset
    dfwine.head()
    
    Out[131]:
    alcohol malic_acid ash alcalinity_of_ash magnesium total_phenols flavanoids nonflavanoid_phenols proanthocyanins color_intensity hue od280/od315_of_diluted_wines proline target
    0 14.23 1.71 2.43 15.6 127.0 2.80 3.06 0.28 2.29 5.64 1.04 3.92 1065.0 0
    1 13.20 1.78 2.14 11.2 100.0 2.65 2.76 0.26 1.28 4.38 1.05 3.40 1050.0 0
    2 13.16 2.36 2.67 18.6 101.0 2.80 3.24 0.30 2.81 5.68 1.03 3.17 1185.0 0
    3 14.37 1.95 2.50 16.8 113.0 3.85 3.49 0.24 2.18 7.80 0.86 3.45 1480.0 0
    4 13.24 2.59 2.87 21.0 118.0 2.80 2.69 0.39 1.82 4.32 1.04 2.93 735.0 0
    In [132]:
    dfwine.iloc[:,:-1] = descriptivos_escalados
    #después de escalar
    dfwine.head()
    
    Out[132]:
    alcohol malic_acid ash alcalinity_of_ash magnesium total_phenols flavanoids nonflavanoid_phenols proanthocyanins color_intensity hue od280/od315_of_diluted_wines proline target
    0 1.518613 -0.562250 0.232053 -1.169593 1.913905 0.808997 1.034819 -0.659563 1.224884 0.251717 0.362177 1.847920 1.013009 0
    1 0.246290 -0.499413 -0.827996 -2.490847 0.018145 0.568648 0.733629 -0.820719 -0.544721 -0.293321 0.406051 1.113449 0.965242 0
    2 0.196879 0.021231 1.109334 -0.268738 0.088358 0.808997 1.215533 -0.498407 2.135968 0.269020 0.318304 0.788587 1.395148 0
    3 1.691550 -0.346811 0.487926 -0.809251 0.930918 2.491446 1.466525 -0.981875 1.032155 1.186068 -0.427544 1.184071 2.334574 0
    4 0.295700 0.227694 1.840403 0.451946 1.281985 0.808997 0.663351 0.226796 0.401404 -0.319276 0.362177 0.449601 -0.037874 0
    Ejercicio: separa los atributos descriptivos y la variable objetivo en los subconjuntos de entrenamiento y test.
    Sugerencia: para separar entre train y test podéis usar "train_test_split" de sklearn.
    In [133]:
    from sklearn.model_selection import train_test_split
    
    X = dfwine.drop(labels='target',axis=1)
    y = dfwine.target
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=42)
    
    Análisis: explica si la decisión de transformar el conjunto de datos (estandarización) antes de realizar la separación del conjunto de datos en los subconjuntos de entrenamiento y test es una buena idea.

    La estandardización consiste en restar el cada instancia de un atributo de la media y dividirlo por la desviación estandar.

    Si aplicamos esta técnica antes de separar la muestra en submuestra de entrenamiento y test, la media y la desviación estandar usadas para la estandardización son las de la muestra completa y contaminaríamos con esta información la muestra de test con la consecuencia de poder introducir sesgo.

    Con la función StandardScaler, es mejor centrar y escalar de manera independiente en cada atributo computando las estadísticas relevantes en las muestras del entrenamiento. La media y la desviacion estandar entonces se guardan para usarlas después en otros datos para su transformación.

    Análisis: En este ejercicio hemos estandarizado los valores de los atributos descriptivos para que sus escalas no sean muy diferentes. ¿Qué nos aporta estandarizar los atributos descriptivos? ¿hay alguna situación o escenario en la que sea imprescindible?

    La estandardización permite convertir la distribución del dataset a una distribución normal estandard de media 0 y desviación estandar 1 que puede reconvertirse a su forma original.

    Esta unificación de escala facilita la labor de los algoritmos de aprender los pesos de los atributos sin necesidad de tomar en cuenta rangos o escalas. En particular la estandardización es útil cuando existen outliers que pueden afectar a las escalas de los valores normalizados. Consigue mantener el valor de los outliers sin que impacten negativamente con sesgos y falta de rendimiento a los modelos.

    Para algunos algoritmos de aprendizaje automático como Suport Vector Machine es imprescindible estandardizar.

    4. Reducción de la dimensionalidad (2.5 puntos)¶

    Con el propósito de comprobar visualmente la distribución de la variable objetivo teniendo en cuenta todos los atributos descriptivos a la vez, vamos a reducir la dimensionalidad del problema a solamente dos atributos que serán la proyección de los atributos descriptivos originales.

    Ejercicio:
    • Aplicad el método de reducción de la dimensionalidad Principal Component Analysis (PCA) para reducir a 2 dimensiones el dataset entero con todas las features.
    • Generad un gráfico en 2D con el resultado del PCA usando colores diferentes para cada una de las clases de la respuesta (wine_class), con el objetivo de visualizar si es posible separar eficientemente las clases con este método.
    NOTA: Tened cuidado, no incluyáis la variable objetivo en la reducción de dimensionalidad. Queremos explicar la variable objetivo en función del resto de variables reducidas a dos dimensiones.
    Sugerencia: no es necesario que programéis el algoritmo de PCA, podéis usar la implementación disponible en la librería de "scikit-learn".
    In [134]:
    from sklearn.decomposition import PCA
    
    #X ya está escalado en el punto 3
    pca = PCA(n_components=2)
    X_w = pca.fit(X).transform(X)
    
    target_names = winedata.target_names
    
    # Percentage of variance explained for each components
    print('explained variance ratio (first two components): %s' % str(pca.explained_variance_ratio_))
    
    plt.figure()
    colors = ['navy', 'turquoise', 'darkorange']
    lw = 2
    
    for color, i, target_name in zip(colors, [0, 1, 2], target_names):
        plt.scatter(X_w[y == i, 0], X_w[y == i, 1], color=color, alpha=.8, lw=lw, label=target_name)
    plt.legend(loc='best', shadow=False, scatterpoints=1)
    plt.title('PCA of Wine dataset')
    
    plt.show()
    
    explained variance ratio (first two components): [0.36198848 0.1920749 ]
    
    Ejercicio:
    • Repetid la reducción de dimensionalidad, pero en este caso usando TSNE. Podéis encontrar más información sobre este algoritmo en el link: https://distill.pub/2016/misread-tsne/
    • Al igual que antes, generad un gráfico en 2D con el resultado del PCA usando colores diferentes para cada una de las clases de la respuesta (wine_class), con el objetivo de visualizar si es posible separar eficientemente las clases con este método.

    Sugerencia: no es necesario que programéis el algoritmo TSNE, podéis usar la implementación disponible en la librería de "scikit-learn".
    Sugerencia: a parte de especificar el número de componentes, probad a usar los parámetros "learning_rate" y "perplexity".
    In [135]:
    from sklearn.manifold import TSNE
    
    tsne = TSNE(n_components=2, perplexity=50, learning_rate=10, n_iter=1000)
    
    X_w3 = tsne.fit_transform(X)
    X_w3.shape
    
    plt.figure()
    colors = ['navy', 'turquoise', 'darkorange']
    lw = 2
    
    for color, i, target_name in zip(colors, [0, 1, 2], target_names):
        plt.scatter(X_w3[y == i, 0], X_w3[y == i, 1], color=color, alpha=.8, lw=lw, label=target_name)
    plt.legend(loc='best', shadow=False, scatterpoints=1)
    plt.title('TSNE Perplexity 50 LR 10 of Wine dataset')
    
    plt.show()
    
    # “The performance of SNE is fairly robust to changes in the perplexity, and typical values are between 5 and 50.” 
    # But the story is more nuanced than that. 
    # Getting the most from t-SNE may mean analyzing multiple plots with different perplexities. Perplexity <= number of points
    # distances between well-separated clusters in a t-SNE plot may mean nothing
    # learning rate (often called “epsilon”) of 10. the most important thing is to iterate until reaching a stable configuration.
    
    In [156]:
    # Bajo perplexity para potenciar estructuras locales - los patrones o relaciones que existen entre puntos de datos cercanos entre sí en el espacio de alta dimensión
    # se vuelve inestable
    
    tsne = TSNE(n_components=2, perplexity=60, learning_rate=10, n_iter=500)
    
    X_w3 = tsne.fit_transform(X)
    X_w3.shape
    
    plt.figure()
    colors = ['navy', 'turquoise', 'darkorange']
    lw = 2
    
    for color, i, target_name in zip(colors, [0, 1, 2], target_names):
        plt.scatter(X_w3[y == i, 0], X_w3[y == i, 1], color=color, alpha=.8, lw=lw, label=target_name)
    plt.legend(loc='best', shadow=False, scatterpoints=1)
    plt.title('TSNE Perplexity 10 LR 30 of Wine dataset')
    
    plt.show()
    
    Análisis:
    Observando los dos gráficos, ¿crees que ha funcionado bien la reducción de dimensionalidad? ¿Ha conseguido separar las clases correctamente? ¿Cuál de los dos métodos ha funcionado mejor? ¿Por qué obtenemos resultados tan diferentes?

    Creo que la reducción ha funcionado bien pues los componentes principales obtenidos parecen contener suficiente varianza como para permitir visualizar las 3 clases.

    En cuanto a los métodos creo que el PCA es más claro y estable.

    Aunque en el primer plot de TSNE en el que utilizo Perplexity 50 y learning rate 10 los cluster se visualizan mejor. El perplexity más bajo potencia las estructuras locales contra las globales, así se aprecian más los patrones entre puntos cercanos. Variar estos parámetros resulta muy complejo y la visualización puede cambiar drásticamente al aumentar la perplexity.

    5. Conjuntos desbalanceados de datos (2.5 puntos)¶

    En los problemas de clasificación, es muy común encontrar conjuntos de datos muy desbalanceados. En la industria existen múltiples ejemplos, como la detección de fraude o la fuga de clientes. Por este motivo, este ejercicio se centra en el análisis de este tipo de conjuntos.

    Vamos a utilizar un conjunto de datos simplificado del data set Turbo Engine del NASA Prognostics Center of Excellence Data Set Repository, el cual sólo estará formado por dos características explicativas, y la variable objetivo, para así poder analizar visualmente el problema de manera sencilla. En este data set tenemos mediciones de los sensores de un motor cada cierto tiempo, siendo la variable objetivo el estado del motor, el cual indica si hay avería o no en el instante de la medición.

    Vamos a comenzar cargando el conjunto de datos:

    In [137]:
    engine_df = pd.read_csv('Turbo_engine.csv', sep=';')
    target_feat = 'y'
    x1_feat = 'x_1'
    x2_feat = 'x_2'
    
    engine_df.head()
    
    Out[137]:
    x_1 x_2 y
    0 1 64182 0
    1 2 64215 0
    2 3 64235 0
    3 4 64235 0
    4 5 64237 0

    A continuación, vamos a analizar la distribución de nuestro conjunto de datos. Para ello, utilizaremos la función show_distribution:

    In [138]:
    def show_distribution(df):
        freq = df[target_feat].value_counts()
        plt.pie(freq, labels=('No engine failure ('+str(freq[0])+')', 'Engine failure ('+str(freq[1])+')'), autopct='%1.1f%%')
        plt.title("Engine failure distribution")
    
    In [139]:
    show_distribution(engine_df)
    

    Cómo se puede observar, el conjunto está muy desbalanceado, ya que sólo 0.5% de las muestras se corresponden con una situación de avería en el motor.

    Aprovechando que sólo tenemos dos características descriptivas, vamos a mostrar mediante un scatter plot nuestro conjunto de datos. Para ello utilizaremos la función plot_data. Esta función recibe tres parámetros:

    • data_sets: Una lista de DataFrames a graficar. Cada uno debe contener las dos características descriptivas, y la clase, ("y", engine failure).
    • only_failures: Parámetro booleano que indica si sólo queremos ver las averías del motor o por el contrario, todo el data set. En este último caso, además se mostrará la frontera de decisión de un clasificador lineal base.
    • cmap: Valor del parámetro "cmap" para el scatter plot.
    In [140]:
    def plot_data(data_sets, only_failures=False, cmap='Paired'):
    
        if not isinstance(data_sets, list):
            data_sets = [data_sets]
    
        colors = np.array(["skyblue", "red"])
        fig, ax = plt.subplots(len(data_sets), 1, figsize=(12, 7 * len(data_sets)))
    
        for i, data in enumerate(data_sets):
            data = data if not only_failures \
                else data[data[target_feat] == 1]
    
            eff_ax = ax if len(data_sets) == 1 else ax[i]
            X = data[[x1_feat, x2_feat]].values
            y = data[target_feat].values
    
            if not only_failures:
                clf = LinearDiscriminantAnalysis()
                clf.fit(X, y)
    
                h = 2
                x_min, x_max = X[:,0].min() - 10*h, X[:,0].max() + 10*h
                y_min, y_max = X[:,1].min() - 10*h, X[:,1].max() + 10*h
    
                xx, yy = np.meshgrid(np.arange(x_min, x_max, h), 
                                     np.arange(y_min, y_max, h))
    
                Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
                Z = Z.reshape(xx.shape)
    
                eff_ax.contourf(xx, yy, Z, cmap=cmap, alpha=0.25)
                eff_ax.contour(xx, yy, Z, colors='black', linewidths=0.7)
    
            eff_ax.scatter(X[:,0], X[:,1], c=colors[y], cmap=cmap, edgecolors='k')
    
    In [141]:
    plot_data([engine_df], only_failures=False)
    

    En la imagen anterior, se puede observar la distribución de nuestro conjunto de datos. Cómo ya habíamos analizado, hay muy pocas averías de motor (puntos rojos). En la imagen, además, se puede ver la frontera de decisión del clasificador base. Esta frontera nos permite ver las áreas que el modelo considera que son de una clase y las que considera que son de otra. Al poner encima los puntos vemos si los clasifica correctamente en el área que les corresponde. En este caso, la frontera divide el conjunto de datos en dos partes, teniendo sólo dos puntos en una de ellas (avería de motor, parte superior).

    Análisis: ¿por qué motivo el clasificador podría estar haciendo esta división? ¿esta separación es más o menos eficiente que predecir siempre la clase 0, es decir, sin avería de motor? ¿qué métrica habría que utilizar en este tipo de problemas? Justificad todas las respuestas.

    El clasificar hace esta división porque no generaliza bien ya que la clase 1 (avería) no está suficientemente representada en el conjunto de entremaniento. El clasificador hace overfitting en esta clase y la clasifica incorrectamente. La métrica que nos indicaría claramente la existencia de overfitting es en el caso de ser la clase avería (1) la negativa: la specificity o tasa de instancias correctamente clasificadas como negativas respecto a todas las instancias negativas. Si en cambio la clase positiva es la clase avería 1 la métrica que usaríamos sería el recall o sensitivity que se corresponden con la tasa de verdaderos positivos (TPR.)

    Para abordar el problema de datos desbalanceados, vamos a analizar la técnica de sobremuestreo (oversampling) de la clase minoritaria. En la literatura hay más técnicas para abordar este problema, como el submuestreo (undersampling) de la clase mayoritaria, pero en esta PEC nos vamos a centrar sólo en esta técnica.

    5.1 Oversampling¶

    Ejercicio: incrementad las muestras de la clase minoritaria hasta alcanzar un número similar de elementos que la clase mayoritaria, aplicando las siguientes técnicas:
    • Duplicación aleatoria (random over-sampling), fijando random_state=10.
    • SMOTE (Synthetic Minority Over-sampling Technique), fijando random_state=10.
    • ADASYN (Adaptive Synthetic Sampling), fijando random_state=10.
    Por último, comprobad el resultado de aplicar cada una de las técnicas utilizando la función show_distribution.
    Sugerencia: para aplicar la duplicación aleatoria podéis usar "RandomOverSampler" de imblearn.
    Sugerencia: para aplicar smote podéis usar "SMOTE" de imblearn.
    Sugerencia: para aplicar adasun podéis usar "ADASYN" de imblearn.
    In [142]:
    from collections import Counter
    
    #X, y = make_classification(n_classes=2, class_sep=2, weights=[0.1, 0.9], n_informative=3, n_redundant=1, flip_y=0, n_features=20, n_clusters_per_class=1, n_samples=1000, random_state=10)
    X = engine_df[[x1_feat, x2_feat]].values
    y = engine_df[target_feat].values
    
    #shape original
    print("Shape of data (número de filas):", engine_df.shape[0] , "\n")
    print('Original dataset shape %s' % Counter(y))
    
    #Duplicación aleatoria (random over-sampling), fijando random_state=10
    from imblearn.over_sampling import RandomOverSampler
    
    ros = RandomOverSampler(random_state=10)
    X_ros, y_ros = ros.fit_resample(X, y)
    print('Resampled dataset shape random %s' % Counter(y_res))
    #creamos un dataframe pandas de nuestro array numpy
    engine_df_ros = pd.DataFrame(data=X_ros, columns=['x_1','x_2'])
    engine_df_ros['y']=y_ros
    show_distribution(engine_df_ros)
    
    #SMOTE (Synthetic Minority Over-sampling Technique), fijando random_state=10
    from imblearn.over_sampling import SMOTE
    
    sm = SMOTE(random_state=10)
    X_smote, y_smote = sm.fit_resample(X, y)
    print('Resampled dataset shape con smote %s' % Counter(y_smote))
    #creamos un dataframe pandas de nuestro array numpy
    engine_df_smote = pd.DataFrame(data=X_smote, columns=['x_1','x_2'])
    engine_df_smote['y']=y_smote
    
    
    #ADASYN (Adaptive Synthetic Sampling), fijando random_state=10
    from imblearn.over_sampling import ADASYN
    
    ada = ADASYN(random_state=10)
    X_ada, y_ada = ada.fit_resample(X, y)
    print('Resampled dataset shape con adasyn %s' % Counter(y_ada))
    #creamos un dataframe pandas de nuestro array numpy
    engine_df_ada = pd.DataFrame(data=X_ada, columns=['x_1','x_2'])
    engine_df_ada['y']=y_ada
    
    Shape of data (número de filas): 20631 
    
    Original dataset shape Counter({0: 20531, 1: 100})
    Resampled dataset shape random Counter({0: 20531, 1: 20531})
    Resampled dataset shape con smote Counter({0: 20531, 1: 20531})
    Resampled dataset shape con adasyn Counter({1: 20565, 0: 20531})
    
    In [143]:
    show_distribution(engine_df_smote)
    
    In [144]:
    show_distribution(engine_df_ada)
    
    Ejercicio: graficad las averías del conjunto de datos original y el obtenido al aplicar Random Over Sampling, utilizando la función plot_data con el parámetro "only_failures" fijado a True.
    In [145]:
    plot_data([engine_df,engine_df_ros], only_failures=True)
    
    Análisis: ¿qué diferencias y similitudes encuentras en las dos imágenes anteriores? Razonad la respuesta teniendo en cuenta la distribución de ambos conjuntos, es decir, el número de averías de motor.

    No se aprecian diferencias en los datasets puesto que la duplicación aleatoria no cambia la distribución suficientemente y los datos sintéticos aparecen superpuestos a los originales.

    Ejercicio: graficad las averías del conjunto de datos original y el obtenido al aplicar Random Over Sampling, utilizando la función plot_data con el parámetro "only_failures" fijado a False.
    In [146]:
    plot_data([engine_df,engine_df_ros], only_failures=False)
    
    Análisis: Teniendo en cuenta el análisis anterior, ¿qué ha pasado con la frontera de decisión? ¿por qué?

    La frontera de la decisión se ha ajustado y las clases avería o negativa tienen son clasificadas correctamente aunque la consecuencia es que hay muchas clases 0 clasificadas ahora incorrectamente.

    Ejercicio: graficad las averías de los tres conjuntos de datos obtenidos al aplicar las tres técnicas de sobremuestreo, utilizando la función plot_data con el parámetro "only_failures" fijado a True.
    In [147]:
    plot_data([engine_df_ros,engine_df_smote,engine_df_ada], only_failures=True)
    
    Análisis: Teniendo en cuenta el número de muestras con avería de motor en cada conjunto, comentad las diferencias y similitudes en las imágenes del ejercicio anterior. Justificad la respuesta teniendo en cuenta el comportamiento de cada una de las técnicas utilizadas.

    Como hemos visto la duplicación aleatoria no modifica cómo se visualizan la clase pues se superpone. En cambio tanto en el oversampling usando SMOTE como ADASYN vemos que los nuevos datos sintéticos siguen una distribución lineal en ambos casos sin percibirse diferencia.

    En SMOTE se calcula el dato sintético usando un método lineal, usando la distancia de un punto con sus k vecinos y ponderando esta distancia para generar el nuevo dato sintético.

    ADASYN genera nuevos datos sintéticos de forma sistemática y adaptativa y no intrínsecamente lineal y basándose en la densidad de la clase, creando más datos sintéticos allí donde hay menos densidad y es más dificil clasificar la clase minoritaria.

    No se visualizan diferencias entre la visualización de SMOTE y ADASYN puesto que ambas se basan en crear los datos sintéticos en las zonas fronterizas de la clase minoritaria para evitar el solape pero en ninguna se realiza penalización sobre las ubicaciones que se superponen o quedan rodeadas de puntos de la clase mayoritaria.

    Ejercicio: graficad las averías de los tres conjuntos de datos obtenidos al aplicar las tres técnicas de sobremuestreo, utilizando la función plot_data con el parámetro "only_failures" fijado a False.
    In [148]:
    plot_data([engine_df_ros,engine_df_smote,engine_df_ada], only_failures=False)
    
    Análisis: Teniendo en cuenta el análisis anterior, ¿qué ha pasado con la frontera de decisión? ¿por qué? Tened en cuenta que el clasificador base utilizado es lineal.

    No se ha modificado la linea de decisión usando oversampling con SMOTE ni ADASYN. Obtenemos el mismo resultado que con la duplicación aleatoria. Esto se debe a que los nuevos datos sintéticos se superponen o están demasiado cercanos a los de la clase mayoritaria y se considerarían ruido.

    El clasificador usado LDA utiliza la media de cada clase, la dispersión intraclase e interclase, utiliza la relación entre estas dos dispersiones para pintar el eje que mejor separa las clases donde después proyectará las nuevas instancias.

    Es posible que las nuevas instancias sintéticas creadas con SMOTE y ADASYN al estar superpuestas con instancias de la clase mayoritaria no estén aportando más que lo ya aportado por la duplicación aleatoria a la separación de clases pues la distribución interclase no varía suficientemente. La distribución interclases no cambia suficientemente.

    LDA presupone una distribución de las clases son gaussianas y con covarianza similares, pero si no se cumplen estas características o las clases están superpuestas como es este el caso, este algoritmo no es suficientemente sensible para separar las clases correctamente.

    Análisis: Para finalizar el ejercicio, imaginad que tenéis que abordar un problema de clasificación binario desbalanceado y utilizáis la técnica SMOTE para balancear el conjunto. ¿esta técnica se debería aplicar antes o después de dividir el conjunto en entrenamiento/validación/test? ¿por qué? Justifica la respuesta.

    Las tecnicas de oversampling como SMOTE que sirven para balancear el conjunto de datos se deben aplicar después de dividir el conjunto de datos en entrenamiento/validación/test.

    Esto se debe a que queremos balancear el conjunto de entrenamiento para que el modelo pueda aprender bien con suficientes ejemplos de las dos clases y pueda generalizar.

    Pero si aplicamos el oversampling antes de dividir los datos corremos el riesgo de tener en el conjunto de test datos sintéticos y duplicados del conjunto de training con lo que la evaluación del modelo quedaría comprometida.

    En el caso de este ejercicio, puesto que no vamos a evaluar el modelo más que con una visualización, no ha sido necesario realizar la separación del dataset en training/test.